--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 3be6cd2113c8f6fb7313111d25a22ac355e27bc8
Parents : da0850f
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-18T05:24:27-05:00
feat(call): add minimize button for active calls, ensure all phone settings persist
Changes
11 files changed, 194 insertions(+), 2 deletions(-)
Diff
diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue
index 4110193c..0b1e71b2 100644
--- a/meshchatx/src/frontend/components/call/CallPage.vue
+++ b/meshchatx/src/frontend/components/call/CallPage.vue
@@ -113,8 +113,63 @@
</div>
<template v-if="config?.telephone_enabled">
+ <!-- Minimized call bar -->
<div
- v-if="activeCall || isCallEnded || initiationStatus"
+ v-if="callMinimized && activeCall"
+ class="w-full flex-shrink-0 border-b border-gray-200 dark:border-zinc-800"
+ >
+ <div
+ class="flex items-center gap-3 px-4 py-2 bg-blue-50/80 dark:bg-blue-900/20 backdrop-blur-sm"
+ >
+ <div class="relative">
+ <div
+ class="size-8 rounded-full bg-gray-100 dark:bg-zinc-800 flex items-center justify-center overflow-hidden"
+ >
+ <LxmfUserIcon
+ :custom-image="activeCall?.custom_image"
+ :icon-name="activeCall?.remote_icon?.icon_name || 'account'"
+ :icon-foreground-colour="activeCall?.remote_icon?.foreground_colour"
+ :icon-background-colour="activeCall?.remote_icon?.background_colour"
+ icon-class="size-7"
+ />
+ </div>
+ <div
+ class="absolute -bottom-0.5 -right-0.5 size-2.5 bg-green-500 rounded-full border-2 border-white dark:border-zinc-900"
+ ></div>
+ </div>
+ <div class="flex-1 min-w-0">
+ <div class="text-sm font-bold text-gray-900 dark:text-white truncate">
+ {{ activeCall?.remote_identity_name || $t("call.unknown") }}
+ </div>
+ <div class="text-[10px] text-gray-500 dark:text-zinc-400 flex items-center gap-2">
+ <span class="size-1.5 bg-green-500 rounded-full animate-pulse"></span>
+ <span>{{ $t("call.active") }}</span>
+ <span v-if="elapsedTime" class="font-mono">· {{ elapsedTime }}</span>
+ </div>
+ </div>
+ <div class="flex items-center gap-1 shrink-0">
+ <button
+ type="button"
+ class="size-8 flex items-center justify-center rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors text-gray-600 dark:text-zinc-400"
+ title="Expand call"
+ @click="callMinimized = false"
+ >
+ <MaterialDesignIcon icon-name="chevron-up" class="size-5" />
+ </button>
+ <button
+ type="button"
+ class="size-8 flex items-center justify-center rounded-full hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors text-red-600 dark:text-red-400"
+ title="Hangup"
+ @click="hangupCall"
+ >
+ <MaterialDesignIcon icon-name="phone-hangup" class="size-4 rotate-135" />
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <!-- Full call UI or settings -->
+ <div v-if="(activeCall || isCallEnded || initiationStatus) && !callMinimized"
class="flex-1 flex flex-col items-center justify-center py-12 px-4"
>
<div
@@ -451,6 +506,16 @@
</button>
</div>
+ <!-- minimize call -->
+ <button
+ type="button"
+ class="flex items-center justify-center gap-2 rounded-2xl bg-gray-100 dark:bg-zinc-800 py-3 px-4 text-sm font-bold text-gray-700 dark:text-zinc-300 hover:bg-gray-200 dark:hover:bg-zinc-700 transition-all duration-200"
+ @click="callMinimized = true"
+ >
+ <MaterialDesignIcon icon-name="chevron-down" class="size-5" />
+ <span>{{ $t("call.minimize") }}</span>
+ </button>
+
<!-- hangup/decline call -->
<button
type="button"
@@ -468,7 +533,7 @@
</div>
</div>
- <div v-else class="space-y-6 my-6 max-w-3xl mx-auto w-full">
+ <div v-if="!((activeCall || isCallEnded || initiationStatus) && !callMinimized)" class="space-y-6 my-6 max-w-3xl mx-auto w-full">
<div class="w-full border-b border-gray-200 dark:border-zinc-800 py-2">
<div class="flex items-center gap-3 mb-6">
<div class="bg-blue-100 dark:bg-blue-900/30 p-2.5 rounded-2xl">
@@ -2324,6 +2389,7 @@ export default {
visualizerEnabled: true,
prevCallTxBytes: 0,
prevCallRxBytes: 0,
+ callMinimized: false,
};
},
computed: {
diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index 84355759..ad3a3be9 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -2278,6 +2278,8 @@
"local_tx": "Lokal (TX)",
"download_rx": "Download RX",
"download_tx": "Download TX",
+ "minimize": "Minimize",
+ "active": "Active",
"default_quality": "Standardqualität",
"active_call": "Aktiver Anruf",
"status": "Status",
diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index 7bf2541b..4ff84988 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -2367,6 +2367,8 @@
"local_tx": "Local (TX)",
"download_rx": "Download RX",
"download_tx": "Download TX",
+ "minimize": "Minimize",
+ "active": "Active",
"default_quality": "Default Quality",
"active_call": "Active Call",
"status": "Status",
diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index db37b6eb..2cc1f7d6 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -2367,6 +2367,8 @@
"local_tx": "Local (TX)",
"download_rx": "Descargar RX",
"download_tx": "Descargar TX",
+ "minimize": "Minimize",
+ "active": "Active",
"default_quality": "Calidad predeterminada",
"active_call": "Llamada activa",
"status": "Situación",
diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index 0ceca1ef..1dfa89b8 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -2367,6 +2367,8 @@
"local_tx": "Local (TX)",
"download_rx": "Télécharger RX",
"download_tx": "Télécharger TX",
+ "minimize": "Minimize",
+ "active": "Active",
"default_quality": "Qualité par défaut",
"active_call": "Appel actif",
"status": "État",
diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index a9998299..35cb1f09 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -2419,6 +2419,8 @@
"local_tx": "Locale (TX)",
"download_rx": "Scarica RX",
"download_tx": "Scarica TX",
+ "minimize": "Minimize",
+ "active": "Active",
"default_quality": "Qualità Predefinita",
"active_call": "Chiamata Attiva",
"status": "Stato",
diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 9804e531..bf5cb848 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -2367,6 +2367,8 @@
"local_tx": "Lokaal (TX)",
"download_rx": "Download RX",
"download_tx": "Download TX",
+ "minimize": "Minimize",
+ "active": "Active",
"default_quality": "Standaardkwaliteit",
"active_call": "Actieve oproep",
"status": "Status",
diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index c7135dd9..c1229564 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -2278,6 +2278,8 @@
"local_tx": "Локальный (TX)",
"download_rx": "Скачать RX",
"download_tx": "Скачать TX",
+ "minimize": "Minimize",
+ "active": "Active",
"default_quality": "Качество по умолчанию",
"active_call": "Активный звонок",
"status": "Статус",
diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index 082747b0..fb0152cc 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -2367,6 +2367,8 @@
"local_tx": "本地(TX)",
"download_rx": "下载 RX",
"download_tx": "下载 TX",
+ "minimize": "Minimize",
+ "active": "Active",
"default_quality": "默认质量",
"active_call": "活动通话",
"status": "状态",
diff --git a/tests/backend/test_config_manager.py b/tests/backend/test_config_manager.py
index 71da1155..07156ec4 100644
--- a/tests/backend/test_config_manager.py
+++ b/tests/backend/test_config_manager.py
@@ -95,6 +95,74 @@ def test_telephony_config(db):
assert config.telephone_enabled.get() is False
+def test_all_telephony_settings_persist(db):
+ """Verify every telephone/call setting survives a manager reload (simulated restart)."""
+ config = ConfigManager(db)
+
+ # Set all telephone-related settings
+ config.telephone_enabled.set(False)
+ config.do_not_disturb_enabled.set(True)
+ config.telephone_allow_calls_from_contacts_only.set(False)
+ config.telephone_announce_enabled.set(True)
+ config.telephone_audio_profile_id.set(5)
+ config.telephone_web_audio_enabled.set(True)
+ config.telephone_web_audio_allow_fallback.set(False)
+ config.call_recording_enabled.set(True)
+ config.telephone_tone_generator_enabled.set(False)
+ config.telephone_tone_generator_volume.set(80)
+
+ # Voicemail settings
+ config.voicemail_enabled.set(True)
+ config.voicemail_auto_answer_delay_seconds.set(15)
+ config.voicemail_max_recording_seconds.set(120)
+ config.voicemail_tts_speed.set(150)
+ config.voicemail_tts_pitch.set(50)
+ config.voicemail_tts_word_gap.set(10)
+ config.voicemail_tts_voice.set("de-de+f1")
+
+ # Ringtone settings
+ config.custom_ringtone_enabled.set(True)
+ config.ringtone_volume.set(75)
+ config.ringtone_preferred_id.set(3)
+
+ # Desktop / misc
+ config.desktop_open_calls_in_separate_window.set(True)
+
+ # Simulate restart: new ConfigManager on same DB
+ config2 = ConfigManager(db)
+
+ # Assert all values persisted
+
+ # Telephone base
+ assert config2.telephone_enabled.get() is False
+ assert config2.do_not_disturb_enabled.get() is True
+ assert config2.telephone_allow_calls_from_contacts_only.get() is False
+ assert config2.telephone_announce_enabled.get() is True
+ assert config2.telephone_audio_profile_id.get() == 5
+ assert config2.telephone_web_audio_enabled.get() is True
+ assert config2.telephone_web_audio_allow_fallback.get() is False
+ assert config2.call_recording_enabled.get() is True
+ assert config2.telephone_tone_generator_enabled.get() is False
+ assert config2.telephone_tone_generator_volume.get() == 80
+
+ # Voicemail
+ assert config2.voicemail_enabled.get() is True
+ assert config2.voicemail_auto_answer_delay_seconds.get() == 15
+ assert config2.voicemail_max_recording_seconds.get() == 120
+ assert config2.voicemail_tts_speed.get() == 150
+ assert config2.voicemail_tts_pitch.get() == 50
+ assert config2.voicemail_tts_word_gap.get() == 10
+ assert config2.voicemail_tts_voice.get() == "de-de+f1"
+
+ # Ringtone
+ assert config2.custom_ringtone_enabled.get() is True
+ assert config2.ringtone_volume.get() == 75
+ assert config2.ringtone_preferred_id.get() == 3
+
+ # Desktop
+ assert config2.desktop_open_calls_in_separate_window.get() is True
+
+
def test_auto_propagation_config(db):
config = ConfigManager(db)
assert config.lxmf_preferred_propagation_node_auto_select.get() is False
diff --git a/tests/frontend/CallPage.test.js b/tests/frontend/CallPage.test.js
index 2f584a77..bd51e1ea 100644
--- a/tests/frontend/CallPage.test.js
+++ b/tests/frontend/CallPage.test.js
@@ -693,4 +693,46 @@ describe("CallPage.vue", () => {
expect(wrapper.vm.prevCallTxBytes).toBe(9999);
expect(wrapper.vm.prevCallRxBytes).toBe(8888);
});
+
+ describe("callMinimized", () => {
+ it("defaults to false", async () => {
+ const wrapper = mountCallPage();
+ await flushPromises();
+ expect(wrapper.vm.callMinimized).toBe(false);
+ });
+
+ it("toggles to true when minimize button clicked", async () => {
+ const wrapper = mountCallPage();
+ await flushPromises();
+ wrapper.vm.activeCall = { status: 6, remote_identity_name: "Test" };
+ await wrapper.vm.$nextTick();
+ wrapper.vm.callMinimized = true;
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.callMinimized).toBe(true);
+ });
+
+ it("shows settings panel when minimized", async () => {
+ const wrapper = mountCallPage();
+ await flushPromises();
+ wrapper.vm.activeCall = { status: 6, remote_identity_name: "Test" };
+ await wrapper.vm.$nextTick();
+ wrapper.vm.callMinimized = true;
+ await wrapper.vm.$nextTick();
+ // When minimized, settings should be visible (active call UI hidden)
+ const phoneTab = wrapper.find("#dnd-toggle");
+ expect(phoneTab.exists()).toBe(true);
+ });
+
+ it("restores full UI on expand", async () => {
+ const wrapper = mountCallPage();
+ await flushPromises();
+ wrapper.vm.activeCall = { status: 6, remote_identity_name: "Test" };
+ await wrapper.vm.$nextTick();
+ wrapper.vm.callMinimized = true;
+ await wrapper.vm.$nextTick();
+ wrapper.vm.callMinimized = false;
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.callMinimized).toBe(false);
+ });
+ });
});
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────